# -*- coding: utf-8 -*-

# tests/test_document.py
# Part of ‘manpage’, a Python library for making Unix manual documents.
#
# Copyright © 2015–2016 Ben Finney <ben+python@benfinney.id.au>
#
# This is free software: see the grant of license at end of this file.

""" Unit tests for manual page document behaviour. """

import sys
import os
import os.path
import collections
import datetime
import textwrap
import unittest
import unittest.mock

__package__ = os.path.basename(os.path.dirname(os.path.abspath(__file__)))
__import__(__package__)
sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import manpage.document


class NamedTuple_TestCaseMixIn:
    """ Mix-in class to add test cases for a `namedtuple` class. """

    def test_instantiate_with_positional_args(self):
        """ An instance should be created when positional args supplied. """
        args = self.test_args.values()
        instance = self.test_class(*args)
        self.assertIsInstance(instance, self.test_class)

    def test_instantiate_with_keyword_args(self):
        """ An instance should be created when keyword args supplied. """
        kwargs = self.test_args
        instance = self.test_class(**kwargs)
        self.assertIsInstance(instance, self.test_class)

    def test_instance_has_expected_items(self):
        """ The instance should have the expected items. """
        instance = self.test_class(**self.test_args)
        expected_items = list(self.test_args.values())
        instance_items = list(instance)
        self.assertEqual(instance_items, expected_items)

    def test_instance_has_expected_attributes(self):
        """ The instance should have the expected attributes. """
        instance = self.test_class(**self.test_args)
        expected_attributes = dict(self.test_args)
        for (name, expected_value) in expected_attributes.items():
            with self.subTest(attribute_name=name):
                attribute_value = getattr(instance, name)
                self.assertEqual(attribute_value, expected_value)


class MetaData_TestCase(unittest.TestCase, NamedTuple_TestCaseMixIn):
    """ Test cases for class `MetaData`. """

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        self.test_class = manpage.document.MetaData
        self.test_args = collections.OrderedDict([
                ('name', "lorem"),
                ('whatis', "ipsum"),
                ('manual', "dolor"),
                ('section', "sit"),
                ('source', "amet"),
                ])


def setup_metadata_instance(testcase):
    """ Set up a test `MetaData` instance. """
    testcase.test_metadata_instance = manpage.document.MetaData(
            "lorem", "ipsum", "dolor", "sit", "amet")


def setup_document_instance(testcase, test_class=None, test_args=None):
    """ Set up a test `Document` instance. """
    if test_class is None:
        test_class = testcase.document_class
    if test_args is None:
        test_args = {}

    if 'metadata' not in test_args:
        setup_metadata_instance(testcase)
        test_args.update({
                'metadata': testcase.test_metadata_instance,
                })

    testcase.test_document_instance = test_class(**test_args)


class Document_content_sections_TestCaseMixIn:
    """ Mix-in class to add test cases for `Document.content_sections`. """

    def test_has_expected_content_sections(self):
        """ Should have expected content sections. """
        standard_section_titles = self.document_class.standard_section_titles
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                setup_document_instance(self)
                content_sections = self.test_document_instance.content_sections
                for section_title in standard_section_titles:
                    with self.subTest(section_title):
                        self.assertIn(section_title, content_sections)

    def test_content_sections_in_expected_sequence(self):
        """ Should have expected content sections. """
        standard_section_titles = self.document_class.standard_section_titles
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                setup_document_instance(self)
                content_sections = self.test_document_instance.content_sections
                self.assertEqual(
                        list(content_sections.keys()),
                        list(standard_section_titles))


class Document_TestCase(
        unittest.TestCase, Document_content_sections_TestCaseMixIn):
    """ Test cases for class `Document`. """

    document_class = manpage.document.Document

    scenarios = [
            ('simple', {
                'test_today_date': datetime.date(1234, 5, 6),
                'expected_created_date': datetime.date(1234, 5, 6),
                }),
            ]

    def patch_date_today(self, scenario):
        """ Patch the `datetime.date.today` function for the scenario. """
        date_patcher = unittest.mock.patch.object(
                datetime, 'date', autospec=True)
        mock_date = date_patcher.start()
        self.addCleanup(date_patcher.stop)

        mock_date.today.return_value = scenario['test_today_date']

    def test_instantiate(self):
        """ An instance should be created. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                setup_document_instance(self)
                self.assertIsInstance(
                        self.test_document_instance, self.document_class)

    def test_has_expected_metadata(self):
        """ Should have expected metadata. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                setup_document_instance(self)
                expected_metadata = self.test_metadata_instance
                self.assertEqual(
                        self.test_document_instance.metadata,
                        expected_metadata)

    def test_has_expected_date(self):
        """ Should have expected creation date. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                self.patch_date_today(scenario)
                setup_document_instance(self)
                self.assertEqual(
                        self.test_document_instance.date,
                        scenario['expected_created_date'])


class Document_as_markup_TestCase(unittest.TestCase):
    """ Test cases for method `Document.as_markup`. """

    document_class = manpage.document.Document

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        setup_document_instance(self)

        self.patch_header()
        self.patch_editor_hints()

    def patch_header(self):
        """ Patch attributes of the header for this test case. """
        header_patcher = unittest.mock.patch.object(
                self.test_document_instance, 'header')
        mock_header = header_patcher.start()
        self.addCleanup(header_patcher.stop)

        mock_header.as_markup.return_value = "lorem ipsum dolor sit amet\n"

    def patch_editor_hints(self):
        """ Patch the `editor_hints` method for this test case. """
        editor_hints_patcher = unittest.mock.patch.object(
                manpage.document.GroffMarkup, 'editor_hints')
        mock_editor_hints = editor_hints_patcher.start()
        self.addCleanup(editor_hints_patcher.stop)

        mock_editor_hints.return_value = "vitae blandit eros\n"

    def make_expected_result(self):
        """ Make an expected result for this test case. """
        header = self.test_document_instance.header
        header_markup = header.as_markup.return_value
        content_sections = [
                section for section in
                self.test_document_instance.content_sections.values()
                if section is not None]
        content_markup = ".\n".join(
                section.as_markup() for section in content_sections)
        footer_markup = "\n".join([
                ".",
                manpage.document.GroffMarkup.editor_hints.return_value])
        result = "".join([header_markup, content_markup, footer_markup])
        return result

    def test_result_is_text(self):
        """ Result should be a text object. """
        result = self.test_document_instance.as_markup()
        self.assertIsInstance(result, type(u""))

    def test_returns_expected_markup(self):
        """ Result should be the expected markup text. """
        result = self.test_document_instance.as_markup()
        expected_result = self.make_expected_result()
        self.assertEqual(result, expected_result)


class Document_insert_section_TestCase(unittest.TestCase):
    """ Test cases for method `Document.insert_section`. """

    document_class = manpage.document.Document

    scenarios = [
            ('index-first', {
                'existing_section_titles': ["DOLOR", "SIT", "AMET"],
                'test_args': {
                    'index': 0,
                    'section': manpage.document.DocumentSection("LOREM"),
                    },
                'expected_section_titles': ["LOREM", "DOLOR", "SIT", "AMET"],
                }),
            ('index-middle', {
                'existing_section_titles': ["DOLOR", "SIT", "AMET"],
                'test_args': {
                    'index': 2,
                    'section': manpage.document.DocumentSection("LOREM"),
                    },
                'expected_section_titles': ["DOLOR", "SIT", "LOREM", "AMET"],
                }),
            ('index-last', {
                'existing_section_titles': ["DOLOR", "SIT", "AMET"],
                'test_args': {
                    'index': 3,
                    'section': manpage.document.DocumentSection("LOREM"),
                    },
                'expected_section_titles': ["DOLOR", "SIT", "AMET", "LOREM"],
                }),
            ('index-end', {
                'existing_section_titles': ["DOLOR", "SIT", "AMET"],
                'test_args': {
                    'index': -1,
                    'section': manpage.document.DocumentSection("LOREM"),
                    },
                'expected_section_titles': ["DOLOR", "SIT", "LOREM", "AMET"],
                }),
            ]

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        setup_document_instance(self)

    def test_resulting_sections_in_expected_sequence(self):
        """ Resulting sections should be in expected sequence. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                test_sections = collections.OrderedDict(
                        (title, manpage.document.DocumentSection(title))
                        for title in scenario['existing_section_titles'])
                self.test_document_instance.content_sections = test_sections
                self.test_document_instance.insert_section(
                        **scenario['test_args'])
                test_sections = self.test_document_instance.content_sections
                self.assertEqual(
                        list(test_sections.keys()),
                        scenario['expected_section_titles'])


class DocumentHeader_TestCase(unittest.TestCase):
    """ Test cases for class `DocumentHeader`. """

    document_class = manpage.document.Document

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        setup_document_instance(self)

    def test_has_specified_document(self):
        """ Should have `document` value specified. """
        instance = manpage.document.DocumentHeader(
                self.test_document_instance)
        self.assertEqual(instance.document, self.test_document_instance)

    def test_has_metadata_from_document(self):
        """ Should have `metadata` value from document. """
        instance = manpage.document.DocumentHeader(
                self.test_document_instance)
        self.assertEqual(
                instance.metadata, self.test_document_instance.metadata)


class DocumentHeader_title_markup_TestCase(unittest.TestCase):
    """ Test cases for method `DocumentHeader.title_markup`. """

    document_class = manpage.document.Document

    scenarios = [
            ('all fields', {
                'metadata': manpage.document.MetaData(
                    name="lorem", whatis=None,
                    manual="dolor", section="sit", source="amet"),
                'test_date': datetime.date(1234, 5, 6),
                'expected_fields': manpage.document.TitleFields(
                    "LOREM", "sit", "1234\\-05\\-06", "amet", "dolor"),
                }),
            ('missing manual', {
                'metadata': manpage.document.MetaData(
                    name="lorem", whatis=None,
                    manual=None, section="sit", source="amet"),
                'test_date': datetime.date(1234, 5, 6),
                'expected_fields': manpage.document.TitleFields(
                    "LOREM", "sit", "1234\\-05\\-06", "amet", None),
                }),
            ('missing source+manual', {
                'metadata': manpage.document.MetaData(
                    name="lorem", whatis=None,
                    manual=None, section="sit", source=None),
                'test_date': datetime.date(1234, 5, 6),
                'expected_fields': manpage.document.TitleFields(
                    "LOREM", "sit", "1234\\-05\\-06", None, None),
                }),
            ]

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        setup_document_instance(self)

        self.mock_title_command = unittest.mock.create_autospec(
                manpage.document.GroffMarkup.title_command,
                return_value=".TH example")
        title_command_patcher = unittest.mock.patch.object(
                manpage.document.GroffMarkup, 'title_command',
                new=self.mock_title_command)
        self.mock_title_command = title_command_patcher.start()
        self.addCleanup(title_command_patcher.stop)

    def test_calls_title_command_with_expected_fields(self):
        """ Should call `title_command` with expected fields. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                self.test_document_instance._created_date = scenario['test_date']
                self.test_document_instance.metadata = scenario['metadata']
                instance = manpage.document.DocumentHeader(
                        self.test_document_instance)
                __ = instance.title_markup()
                self.mock_title_command.assert_called_with(
                        scenario['expected_fields'])


class DocumentHeader_as_markup_TestCase(unittest.TestCase):
    """ Test cases for method `DocumentHeader.as_markup`. """

    document_class = manpage.document.Document

    scenarios = [
            ('simple', {
                'test_args': {
                    'encoding': "litora",
                    },
                'test_title_markup': "lorem ipsum dolor sit amet",
                'expected_result': "lorem ipsum dolor sit amet",
                }),
            ]

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        setup_document_instance(self)

        title_markup_patcher = unittest.mock.patch.object(
                manpage.document.DocumentHeader, 'title_markup')
        title_markup_patcher.start()
        self.addCleanup(title_markup_patcher.stop)

    def test_returns_expected_result(self):
        """ Should return expected markup for inputs. """
        instance = manpage.document.DocumentHeader(self.test_document_instance)
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                instance.title_markup.return_value = scenario[
                        'test_title_markup']
                result = instance.as_markup(**scenario['test_args'])
                self.assertEqual(scenario['expected_result'], result)


class DocumentSection_TestCase(unittest.TestCase):
    """ Test cases for class `DocumentSection`. """

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        self.test_args = {
                'title': "LOREM IPSUM",
                'body': textwrap.dedent("""\
                    Dolor sit amet.
                    """),
                }

    def test_has_specified_title(self):
        """ Should have `title` value specified. """
        instance = manpage.document.DocumentSection(**self.test_args)
        self.assertEqual(instance.title, self.test_args['title'])

    def test_has_specified_body(self):
        """ Should have `body` value specified. """
        instance = manpage.document.DocumentSection(**self.test_args)
        self.assertEqual(instance.body, self.test_args['body'])


class DocumentSection_as_markup_TestCase(unittest.TestCase):
    """ Test cases for method `DocumentSection.as_markup`. """

    scenarios = [
            ('simple', {
                'test_args': {
                    'title': "LOREM IPSUM",
                    'body': textwrap.dedent("""\
                        Dolor sit amet.
                        """),
                    },
                'expected_result': textwrap.dedent("""\
                    .SH LOREM IPSUM
                    Dolor sit amet.
                    """),
                }),
            ('body-no-trailing-newline', {
                'test_args': {
                    'title': "LOREM IPSUM",
                    'body': textwrap.dedent("""\
                        Dolor sit amet."""),
                    },
                'expected_result': textwrap.dedent("""\
                    .SH LOREM IPSUM
                    Dolor sit amet.
                    """),
                }),
            ('body-none', {
                'test_args': {
                    'title': "LOREM IPSUM",
                    'body': None,
                    },
                'expected_result': textwrap.dedent("""\
                    .SH LOREM IPSUM
                    """),
                }),
            ]

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

        title_markup_patcher = unittest.mock.patch.object(
                manpage.document.DocumentHeader, 'title_markup')
        title_markup_patcher.start()
        self.addCleanup(title_markup_patcher.stop)

    def test_returns_expected_result(self):
        """ Should return expected markup for inputs. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                test_args = scenario['test_args']
                instance = manpage.document.DocumentSection(**test_args)
                result = instance.as_markup()
                self.assertEqual(scenario['expected_result'], result)


class CommandDocument_TestCase(
        unittest.TestCase, Document_content_sections_TestCaseMixIn):
    """ Test cases for class `CommandDocument`. """

    document_class = manpage.document.CommandDocument

    scenarios = [
            ('section-specified', {
                'test_args': {
                    'metadata': manpage.document.MetaData(
                        name="lorem", whatis="ipsum",
                        manual="dolor", section="sit",
                        source="amet"),
                    },
                'expected_metadata': manpage.document.MetaData(
                    name="lorem", whatis="ipsum",
                    manual="dolor", section="sit",
                    source="amet"),
                }),
            ('section-default', {
                'test_args': {
                    'metadata': manpage.document.MetaData(
                        name="lorem", whatis="ipsum",
                        manual="dolor", section=None,
                        source="amet"),
                    },
                'expected_metadata': manpage.document.MetaData(
                    name="lorem", whatis="ipsum",
                    manual="dolor", section="1",
                    source="amet"),
                }),
            ]

    def setUp(self):
        """ Set up test fixtures. """
        super().setUp()

    def test_instantiate(self):
        """ An instance should be created. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                test_args = scenario['test_args']
                setup_document_instance(self, test_args=scenario['test_args'])
                self.assertIsInstance(
                        self.test_document_instance, self.document_class)

    def test_has_expected_metadata_value(self):
        """ The document `metadata` should have expected value. """
        for (scenario_name, scenario) in self.scenarios:
            with self.subTest(scenario=scenario_name):
                test_args = scenario['test_args']
                setup_document_instance(self, test_args=scenario['test_args'])
                self.assertEqual(
                        self.test_document_instance.metadata,
                        scenario['expected_metadata'])


# This is free software: you may copy, modify, and/or distribute this work
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; version 3 of that license or any later version.
#
# No warranty expressed or implied. See the file ‘LICENSE.GPL-3’ for details,
# or view it online at <URL:https://www.gnu.org/licenses/gpl-3.0.html>.


# Local variables:
# coding: utf-8
# mode: python
# End:
# vim: fileencoding=utf-8 filetype=python :
